# django-openid-auth -  OpenID integration for django.contrib.auth
#
# Copyright (C) 2008-2009 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""Glue between OpenID and django.contrib.auth."""

__metaclass__ = type

from datetime import datetime

from django.conf import settings
from django.contrib.auth.models import Group
from openid.consumer.consumer import SUCCESS
from openid.extensions import sreg, ax

from models import UserOpenID, User

class AXTypes:
    email = 'http://axschema.org/contact/email'
    username = 'http://axschema.org/namePerson/friendly'
    full_name = 'http://axschema.org/namePerson'
    birth_date = 'http://axschema.org/birthDate/'


class IdentityAlreadyClaimed(Exception):
    pass


class OpenIDBackend:
    """A django.contrib.auth backend that authenticates the user based on
    an OpenID response."""

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

    def authenticate(self, **kwargs):
        """Authenticate the user based on an OpenID response."""
        # Require that the OpenID response be passed in as a keyword
        # argument, to make sure we don't match the username/password
        # calling conventions of authenticate.

        openid_response = kwargs.get('openid_response')

        if openid_response is None:
            return None

        if openid_response.status != SUCCESS:
            return None

        user = None
        try:
            user_openid = UserOpenID.objects.get(
                claimed_id__exact=openid_response.identity_url)
        except UserOpenID.DoesNotExist:
            if getattr(settings, 'OPENID_CREATE_USERS', False):
                user = self.create_user_from_openid(openid_response)
        else:
            user = user_openid.user

        if user is None:
            return None

        if getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False):
            # TODO: check also 'create_user_from_openid', may be unite this section
            sreg_response = sreg.SRegResponse.fromSuccessResponse(
                openid_response)
            ax_response = ax.FetchResponse.fromSuccessResponse(openid_response)
            if sreg_response:
                details = self.get_sreg_parameters(sreg_response)
            elif ax_response:
                details = self.get_ax_parameters(ax_response)
            self.update_user_details(user, details)

        return user

    def get_sreg_parameters(self, sreg_response):
        details = { 'display_name' : sreg_response.get('nickname'), 'email' : sreg_response.get('email', ''),
            'fullname' : sreg_response.get('fullname'), 'birth_date' : sreg_response.get('dob') }
        return details

    def get_ax_parameters(self, ax_response):
        details = { 'display_name' : ax_response.getSingle(AXTypes.username),
            'email' : ax_response.getSingle(AXTypes.email), 'fullname' : ax_response.getSingle(AXTypes.full_name),
            'birth_date' : ax_response.getSingle(AXTypes.birth_date) }
        return details

    def update_user_details(self, user, details):
        fullname = details['fullname']
        if fullname:
            # Do our best here ...
            if ' ' in fullname:
                user.first_name, user.last_name = fullname.rsplit(None, 1)
            else:
                user.first_name = u''
                user.last_name = fullname

        email = details['email']
        if email:
            user.email = email

        profile = user.get_profile()
        if details.get('display_name'):
            profile.display_name = details['display_name']
        else:
            profile.display_name = details['fullname']
        if details['birth_date']:
            profile.birth_date = datetime.strptime(details['birth_date'], '%Y-%m-%d')

        user.save()
        profile.save()

    def create_user_from_openid(self, openid_response):
        details = { 'display_name' : None, 'email' : '', 'fullname' : None, 'birth_date' : None }
        if getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False):
            sreg_response = sreg.SRegResponse.fromSuccessResponse(openid_response)
            ax_response = ax.FetchResponse.fromSuccessResponse(openid_response)
            if sreg_response:
                details = self.get_sreg_parameters(sreg_response)
            elif ax_response:
                details = self.get_ax_parameters(ax_response)

        # Pick a username for the user based on their nickname,
        # checking for conflicts.
        i = 1
        while True:
            if details['display_name']:
                username = details['display_name']
            else:
                username = 'openiduser'
            if i > 1:
                username += str(i)
            try:
                User.objects.get(username__exact=username)
            except User.DoesNotExist:
                break
            i += 1

        user = User.objects.create_user(username, details['email'], password=None)

        if getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False):
            if sreg_response or ax_response:
                self.update_user_details(user, details)

        self.associate_openid(user, openid_response)
        return user

    def associate_openid(self, user, openid_response):
        """Associate an OpenID with a user account."""
        # Check to see if this OpenID has already been claimed.
        try:
            user_openid = UserOpenID.objects.get(
                claimed_id__exact=openid_response.identity_url)
        except UserOpenID.DoesNotExist:
            user_openid = UserOpenID(
                user=user,
                claimed_id=openid_response.identity_url,
                display_id=openid_response.endpoint.getDisplayIdentifier())
            user_openid.save()
        else:
            if user_openid.user != user:
                raise IdentityAlreadyClaimed(
                    "The identity %s has already been claimed"
                    % openid_response.identity_url)

        return user_openid

